Skip to content

feat: bind exposition writers to negotiated escaping - #2642

Open
zeitlinger wants to merge 6 commits into
mainfrom
fix/negotiated-exposition
Open

zeitlinger wants to merge 6 commits into
mainfrom
fix/negotiated-exposition

Conversation

@zeitlinger

@zeitlinger zeitlinger commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

I added an immutable ExpositionFormatWriterWithEscaping wrapper and a factory that binds the selected format to the escaping scheme negotiated from the Accept header. The scrape handler now uses the negotiated writer, while the existing writer API and standard content types remain unchanged. Fixes #1577.

I added the generated API diff report and normalized its generator to omit trailing blank lines rejected by lint. Lint and unit tests pass. The local mise run build reached integration tests but Docker/Testcontainers failures prevented the full build; the install build with integration tests skipped passed.

I also documented manual negotiated responses in the Formats guide.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ API changes detected — maintainer review required

This PR modifies the published API diff for the following module(s):

  • prometheus-metrics-exposition-textformats

Please review the changes in docs/apidiffs/current_vs_latest/ carefully before approving.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
@zeitlinger
zeitlinger marked this pull request as ready for review November 6, 2026 15:24

@jaydeluca jaydeluca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this matches the shape sketched in #1577, and moving the OM2 fallback into
ExpositionFormats means manual callers get it too, not just the scrape handler.

  1. Escaping is still parsed from the whole Accept header rather than the selected media type
    (inline below). Pre-existing, but this method now promises both come from one negotiation.
  2. A couple of small javadoc/test wording nits.

fallback = EscapingScheme.ALLOW_UTF8;
}
return new ExpositionFormatWriterWithEscaping(
writer, EscapingScheme.fromAcceptHeader(acceptHeader, fallback));

@jaydeluca jaydeluca Nov 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EscapingScheme.fromAcceptHeader splits only on ;, so when escaping= is the last parameter of
a media type it absorbs the next one and is ignored:

application/openmetrics-text;version=1.0.0;escaping=allow-utf-8,text/plain;version=0.0.4;q=0.5
  -> OpenMetrics 1.0 writer, UNDERSCORE_ESCAPING

It also lets an escaping= on a media type that wasn't selected apply to the one that was.
Prometheus's own header works because ;q= follows escaping=, and main behaves the same, so
this isn't a regression. But since this method is now the single place where format and escaping
are negotiated, could it read escaping from the media type that findWriter matched (split on
, first)?

* ExpositionFormats.init().findWriterWithEscaping(acceptHeader).write(out, registry.scrape());
* }</pre>
*
* @see #findWriter(String) the legacy API for selecting only a format writer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findWriter isn't deprecated and the docs say it "remains available", so "legacy" sends a mixed
signal.

Suggested change
* @see #findWriter(String) the legacy API for selecting only a format writer
* @see #findWriter(String) to select only the format writer

Comment on lines +306 to +308
// Negotiation binds a scheme to this wrapper without changing the cached format writer.
assertThat(formats.findWriterWithEscaping("text/plain").getEscapingScheme())
.isEqualTo(EscapingScheme.DEFAULT);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion checks a new wrapper's scheme, not that the cached writer is unchanged. To test
what the comment says:

Suggested change
// Negotiation binds a scheme to this wrapper without changing the cached format writer.
assertThat(formats.findWriterWithEscaping("text/plain").getEscapingScheme())
.isEqualTo(EscapingScheme.DEFAULT);
// Negotiation wraps the shared format writer rather than replacing it.
assertThat(formats.findWriter("text/plain; escaping=" + escaping))
.isSameAs(formats.getPrometheusTextFormatWriter());

* format's standard content type and does not include an escaping parameter.
*/
@StableApi
public final class ExpositionFormatWriterWithEscaping {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is @StableApi: was leaving out an accessor for the underlying ExpositionFormatWriter
deliberate? Without one, callers can't check isAvailable() or which format was picked other
than by comparing content types. Adding it later is non-breaking, so fine either way.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Escaping API inconsistencies

2 participants